package org.ozsoft.texasholdem.bots;

import java.io.Serializable;
import java.util.*;
import java.math.*;

import org.ozsoft.texasholdem.Action;
import org.ozsoft.texasholdem.Card;
import org.ozsoft.texasholdem.Hand;
import org.ozsoft.texasholdem.Player;

import UofAHandEval.HandEvaluator;

/**
 * CS 175 Group 2's implementation of texas holdem bot.
 * 
 * @author Group 2
 */
public class CS175Bot extends Bot {

	private final boolean BLUFF_ENABLED = true;
	
	private int bigBlind;
	private List<Player> players;
	private Player dealer;
	private Player actor;
	private List<Card> board;
	private Player player;
	private int bet;
	private int pot;
	private Hashtable<String, LearningTree> opponentLearning = new Hashtable<String, LearningTree>();
	private HandEvaluator handEval = new HandEvaluator();
	private int[][] boardRanks;
	private int handsPlayed = 0;
	private int showdowns = 0;
	private int numOfWins = 0;
	private int numPlayers = -1;
	private double oldPct = 0;
	private boolean played100hands = false;
	private boolean bluff = false;
	
	/*
	 * (non-Javadoc)
	 * 
	 * @see th.Client#messageReceived(java.lang.String)
	 */
	@Override
	public void messageReceived(String message) {
		if(message.contains("New hand"))
		{
			handsPlayed++;
			bluff = getBluff();
		}
		else if(message.equals("Showdown!"))
		{
			showdowns++;
		}
		else if(message.contains("Winner") && message.contains(player.toString()))
			numOfWins++;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see th.Client#joinedTable(int, java.util.List)
	 */
	@Override
	public void joinedTable(int bigBlind, List<Player> players) {
		this.bigBlind = bigBlind;
		this.players = players;
		for(Player p : players)
		{
			opponentLearning.put(p.toString(),new LearningTree());
			numPlayers++;
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see th.Client#handStarted(th.Player)
	 */
	@Override
	public void handStarted(Player dealer) {
		this.dealer = dealer;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see th.Client#actorRotated(th.Player)
	 */
	@Override
	public void actorRotated(Player actor) {
		this.actor = actor;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see th.Client#boardUpdated(java.util.List, int, int)
	 */
	@Override
	public void boardUpdated(List<Card> cards, int bet, int pot) {
		board = cards;
		this.bet = bet;
		this.pot = pot;
		Hand boardHand = new Hand();
		boardHand.addCards(board);
		if(!board.isEmpty())
			boardRanks = handEval.getRanks(new UofAHandEval.Hand(boardHand.toString()));
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.ozsoft.texasholdem.Client#playerUpdated(org.ozsoft.texasholdem.Player
	 * )
	 */
	@Override
	public void playerUpdated(Player player) {
		this.player = player;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see th.Client#playerActed(th.Player)
	 */
	@Override
	public void playerActed(Player player) {
		opponentLearning.get(player.toString()).checkSequence(player.getActions());
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see th.Client#act(java.util.Set)
	 */
	@Override
	public Action act(Set<Action> actions) {
		Hand hand = new Hand();
		hand.addCards(board);
		hand.addCards(player.getCards());
		
		if(board.isEmpty()) //check if pre-flop betting.
		{
			double points = billChenFormula(hand);
			if(points >= 9)
			{
				if(Math.random() <= 0.75)
				{
					if (actions.contains(Action.RAISE)) {
						return Action.RAISE;
					} else if (actions.contains(Action.BET)){
						return Action.BET;
					} else if (actions.contains(Action.CALL)){
						return Action.CALL;
					} else if (actions.contains(Action.CHECK)){
						return Action.CHECK;
					} else {
						return Action.FOLD; 
					}
				}
				else
				{
					if (actions.contains(Action.CHECK)) {
						return Action.CHECK;
					} else {
						return Action.CALL;
					}
				}
			}
			else if(points >= 7)
			{		
				if (actions.contains(Action.CHECK)) {
					return Action.CHECK;
				} else {
					return Action.CALL;
				}
			}
			else
			{
				if (actions.contains(Action.CHECK)) {
					return Action.CHECK;
				} else {
					if(!bluff && !BLUFF_ENABLED)
						return Action.FOLD;
					else {
						if (actions.contains(Action.RAISE)) {
							return Action.RAISE;
						} else if (actions.contains(Action.BET)){
							return Action.BET;
						} else if (actions.contains(Action.CALL)){
							return Action.CALL;
						} else if (actions.contains(Action.CHECK)){
							return Action.CHECK;
						} else {
							return Action.FOLD; 
						}
					}
				}
			}		
		}
		else
		{
			Card[] card = player.getCards();
			int handRank = boardRanks[new UofAHandEval.Card(card[0].toString()).getIndex()][new UofAHandEval.Card(card[1].toString()).getIndex()];
			
			int maxRank = -1;
			int minRank = 9999999;
			for (int[] row : boardRanks)
			{
			    for (int value : row)
			    {
			         if(value > maxRank)
			        	 maxRank = value;
			         if(value != 0 && value < minRank)
			        	 minRank = value;
			    }
			}
			double EHS = (double)handRank/(maxRank-minRank);
			double raisePotOdds = (double)bet/(pot + bet);
			
			double foldRatio = Math.tanh((.5+raisePotOdds)/2 - EHS);
			double raiseRatio = Math.tanh((EHS * 2)-raisePotOdds);
			double callRatio = 1 - (foldRatio + raiseRatio);
			double rand = (foldRatio + raiseRatio + callRatio) * Math.random();
			
			if(rand <= foldRatio)
			{
				if (actions.contains(Action.CHECK)) {
					return Action.CHECK;
				} else {
					if(!bluff && !BLUFF_ENABLED)
						return Action.FOLD;
					else {
						if (actions.contains(Action.RAISE)) {
							return Action.RAISE;
						} else if (actions.contains(Action.BET)){
							return Action.BET;
						} else if (actions.contains(Action.CALL)){
							return Action.CALL;
						} else if (actions.contains(Action.CHECK)){
							return Action.CHECK;
						} else {
							return Action.FOLD; 
						}
					}
				}
			}
			else if(rand <= foldRatio+raiseRatio)
			{
				if (actions.contains(Action.RAISE)) {
					return Action.RAISE;
				} else if (actions.contains(Action.BET)){
					return Action.BET;
				} else if (actions.contains(Action.CALL)){
					return Action.CALL;
				} else if (actions.contains(Action.CHECK)){
					return Action.CHECK;
				} else {
					return Action.FOLD; 
				}
			}
			else
			{		
				if (actions.contains(Action.CHECK)) {
					return Action.CHECK;
				} else {
					return Action.CALL;
				}
			}
		}
	}
	
	//Bill Chen's Formula http://www.simplyholdem.com/chen.html
	private double billChenFormula(Hand hand)
	{
		double points = 0;
		Card[] cards = hand.getCards();
		
		int oneRank = cards[0].getRank();
		int twoRank = cards[1].getRank();
		
		if(oneRank == 12)
			points += 10;
		else if(oneRank == 11)
			points += 8;
		else if(oneRank == 10)
			points += 7;
		else if(oneRank == 9)
			points += 6;
		else
			points += (oneRank+2)/2;
		
		int rankDifference = oneRank - twoRank;
		
		if(rankDifference == 0)
		{
			points *= 2;
			if(points < 5)
				points = 5;
		}
		else if(rankDifference == 1)
			points += 1;
		else if(rankDifference == 2)
			points -= 1;
		else if(rankDifference == 3)
			points -= 2;
		else if(rankDifference == 4)
			points -= 4;
		else
			points -= 5;
		
		if(cards[0].getSuit() == cards[1].getSuit())
			points += 2;
		
		return points;
	}
	private boolean getBluff()
	{
		int clusterSize = 100;
		double winPct;
		double bluffChance = 1 - (double)showdowns/handsPlayed;
		double raisePotOdds = (double)bet/(pot+bet);
		
		if(raisePotOdds < 1-(numPlayers*bluffChance))
		{
			if(!played100hands)
				winPct = (double)numOfWins/handsPlayed;
			else
			{
				winPct = (oldPct*(clusterSize-handsPlayed) + numOfWins)/clusterSize;
			}
			if(handsPlayed>=clusterSize)
			{
				played100hands = true;
				oldPct = winPct;
				handsPlayed = 0;
				numOfWins = 0;
			}
			if(Math.tanh(winPct)>Math.random())
				return true;
		}
		return false;
	}

}
